package com.sl.ms.dispatch.job;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSON;
import cn.hutool.json.JSONUtil;
import com.sl.ms.base.api.common.MQFeign;
import com.sl.ms.base.api.truck.TruckPlanFeign;
import com.sl.ms.base.domain.truck.TruckDto;
import com.sl.ms.base.domain.truck.TruckPlanDto;
import com.sl.ms.dispatch.dto.DispatchMsgDTO;
import com.sl.transport.common.constant.Constants;
import com.xxl.job.core.context.XxlJobHelper;
import com.xxl.job.core.handler.annotation.XxlJob;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * 调度运输任务
 */
@Component
@Slf4j
public class DispatchJob {

    @Resource
    private TruckPlanFeign truckPlanFeign;

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Resource
    private MQFeign mqFeign;


    @Value("${sl.volume.ratio:0.95}")
    private Double volumeRatio;

    @Value("${sl.weight.ratio:0.95}")
    private Double weightRatio;

    /**
     * 分片广播方式处理运单，生成运输任务
     */
    @XxlJob("transportTask")
    public void transportTask() {
        // 分片参数
        int shardIndex = XxlJobHelper.getShardIndex();
        int shardTotal = XxlJobHelper.getShardTotal();

        //TODO 待实现

        // 1.根据分片信息 查询两小时内发车计划
        List<TruckPlanDto> truckPlanDtos = truckPlanFeign.pullUnassignedPlan(shardTotal, shardIndex);
        if (CollUtil.isEmpty(truckPlanDtos)) {
            log.info("未查询到即将发车的 车辆计划");

            return;
        }
        // 2.循环遍历车辆计划
        for (TruckPlanDto truckPlanDto : truckPlanDtos) {
            // 2.1.检查车辆完整性  网点id  车次id 计划id
            if (ObjectUtil.hasEmpty(truckPlanDto.getId(), truckPlanDto.getTransportLineId(), truckPlanDto.getStartOrganId(), truckPlanDto.getEndOrganId())) {
                log.error("车辆计划不符合 要求 车辆信息:{}", truckPlanDto);
                continue;
            }
            // 2.2.获取出发到达网点id
            Long startOrganId = truckPlanDto.getStartOrganId();
            Long endOrganId = truckPlanDto.getEndOrganId();

            // 2.3.获取redis中的key
            String listRedisKey = getListRedisKey(startOrganId, endOrganId);
            // 2.4.创建货物集合
            ArrayList<DispatchMsgDTO> dispatchMsgDTOS = new ArrayList<DispatchMsgDTO>();

            // 2.5.递归匹配运力
            this.executeTransportTask(listRedisKey, truckPlanDto.getTruckDto(), dispatchMsgDTOS);

            // 2.6.生成运输方法
            this.createTransportTask(truckPlanDto, startOrganId, endOrganId, dispatchMsgDTOS);
        }

        // 2.7.发消息表示车辆已经完成调度
        this.completeTruckPlan(truckPlanDtos);
    }

    /**
     * 发消息通知base微服务将 处理过的车辆计划 改为已经调度
     *
     * @param truckPlanDtos
     */
    private void completeTruckPlan(List<TruckPlanDto> truckPlanDtos) {


    }

    /**
     * 创建运输任务
     *
     * @param truckPlanDto
     * @param startOrganId
     * @param endOrganId
     * @param dispatchMsgDTOS
     */
    private void createTransportTask(TruckPlanDto truckPlanDto, Long startOrganId, Long endOrganId, ArrayList<DispatchMsgDTO> dispatchMsgDTOS) {
        //将运单合并的结果以消息的方式发送出去
        //key-> 车辆id，value ->  运单id列表
        //{"driverId":123, "truckPlanId":456, "truckId":1210114964812075008,"totalVolume":4.2,"endOrganId":90001,"totalWeight":7,
        // "transportOrderIdList":[320733749248,420733749248],"startOrganId":100280}

        List<String> transportOrderIdList = dispatchMsgDTOS.stream().map(DispatchMsgDTO::getTransportOrderId).collect(Collectors.toList());

        List<Long> driverIds = CollUtil.isEmpty(truckPlanDto.getDriverIds()) || truckPlanDto.getDriverIds() == null ? ListUtil.empty() : truckPlanDto.getDriverIds();

        Map<Object, Object> msgResult = MapUtil.builder()
                .put("truckId", truckPlanDto.getTruckId()) //车辆id
                .put("driverIds", driverIds) //司机id
                .put("truckPlanId", truckPlanDto.getId()) //车辆计划id
                .put("transportTripsId", truckPlanDto.getTransportTripsId()) //车次id
                .put("startOrganId", startOrganId) //开始机构id
                .put("endOrganId", endOrganId) //结束机构id
                //运单id列表
                .put("transportOrderIdList", transportOrderIdList)
                //总重量
                .put("totalWeight", dispatchMsgDTOS.stream()
                        .mapToDouble(DispatchMsgDTO::getTotalWeight)
                        .sum())
                //总体积
                .put("totalVolume", dispatchMsgDTOS.stream()
                        .mapToDouble(DispatchMsgDTO::getTotalVolume)
                        .sum())
                .build();

        //发送消息
        String jsonMsg = JSONUtil.toJsonStr(msgResult);
        this.mqFeign.sendMsg(Constants.MQ.Exchanges.TRANSPORT_TASK,
                Constants.MQ.RoutingKeys.TRANSPORT_TASK_CREATE, jsonMsg);


        if (CollUtil.isNotEmpty(transportOrderIdList)) {
            // 将redis中set集合连的值删除掉  因为已经没有这些运输订单了
            String setRedisKey = getSetRedisKey(truckPlanDto.getStartOrganId(), truckPlanDto.getEndOrganId());
            // 将集合变为数组
            this.stringRedisTemplate.opsForSet().remove(setRedisKey, transportOrderIdList.toArray());
        }
        
    }

    /**
     * 递归来实现 货物和车的匹配
     *
     * @param listRedisKey
     * @param truckDto
     * @param dispatchMsgDTOS
     */
    private void executeTransportTask(String listRedisKey, TruckDto truckDto, ArrayList<DispatchMsgDTO> dispatchMsgDTOS) {
        // 1.找到list的redis 然后从右侧取货
        String redisData = stringRedisTemplate.opsForList().rightPop(listRedisKey);
        if (StrUtil.isEmpty(redisData)) {
            return;
        }

        // 2.转为对象
        DispatchMsgDTO dispatchMsgDTO = JSONUtil.toBean(redisData, DispatchMsgDTO.class);

        // 3.计算该车辆已经分配的运单，是否超出其运力，载重 或 体积超出，需要将新拿到的运单加进去后进行比较
        // 为什么要转为BigDecimal因为他的maxAllowWeight是BigDecimal的类型
        BigDecimal totalWeight = NumberUtil.add(NumberUtil.toBigDecimal(dispatchMsgDTOS.stream()
                .mapToDouble(DispatchMsgDTO::getTotalWeight)
                .sum()), dispatchMsgDTO.getTotalWeight());

        BigDecimal totalVolume = NumberUtil.add(NumberUtil.toBigDecimal(dispatchMsgDTOS.stream()
                .mapToDouble(DispatchMsgDTO::getTotalVolume)
                .sum()), dispatchMsgDTO.getTotalVolume());

        // 4.车辆最大的容积和载重要留有余量，否则可能会超重 或 装不下
        BigDecimal maxAllowableLoad = NumberUtil.mul(truckDto.getAllowableLoad(), weightRatio);
        BigDecimal maxAllowableVolume = NumberUtil.mul(truckDto.getAllowableVolume(), volumeRatio);

        // 5.判断是否超出
        // isGreaterOrEqual这个是大于等于并且是BigDecimal
        if (NumberUtil.isGreaterOrEqual(totalWeight, maxAllowableLoad)
                || NumberUtil.isGreaterOrEqual(totalVolume, maxAllowableVolume)) {
            //5.1.超出车辆运力，需要取货的运单再放回去，放到最右边，以便保证运单处理的顺序
            this.stringRedisTemplate.opsForList().rightPush(listRedisKey, redisData);
            return;
        }

        //没有超出运力，将该运单加入到集合中
        dispatchMsgDTOS.add(dispatchMsgDTO);

        // 然后再次递归就可以了
        this.executeTransportTask(listRedisKey, truckDto, dispatchMsgDTOS);
    }

    /**
     * 生成redis前缀List类型
     *
     * @param startId
     * @param endId
     * @return
     */
    public String getListRedisKey(Long startId, Long endId) {
        return StrUtil.format("DISPATCH_LIST_{}_{}", startId, endId);
    }

    /**
     * 生成redis前缀 set类型
     *
     * @param startId
     * @param endId
     * @return
     */
    public String getSetRedisKey(Long startId, Long endId) {
        return StrUtil.format("DISPATCH_SET_{}_{}", startId, endId);
    }
}
